package com.sunday.authorization.security.authorization.redis;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sunday.authorization.security.authorization.redis.module.OAuth2AuthorizationModule;
import com.sunday.authorization.security.tools.authorization.redis.constants.RedisOAuth2Constants;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.converter.json.SpringHandlerInstantiator;
import org.springframework.lang.Nullable;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.util.Assert;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 是否跳过授权确认信息，如果值则不进行确认
 *
 * @author sunday
 * @see InMemoryOAuth2AuthorizationConsentService
 * @since 2024/9/13
 */
public final class RedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {

    private StringRedisTemplate stringRedisTemplate;

    private ObjectMapper objectMapper = new ObjectMapper();

    public RedisOAuth2AuthorizationConsentService(StringRedisTemplate stringRedisTemplate,
                                                  AutowireCapableBeanFactory beanFactory

    ) {
        this.stringRedisTemplate = stringRedisTemplate;

        ClassLoader loader = getClass().getClassLoader();
        List<Module> modules = SecurityJackson2Modules.getModules(loader);
        this.objectMapper.registerModules(modules);
        /**
         * 添加spring默认已实现支持
         */
        this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
        /**
         * 添加项目所需要得自定义实现
         */
        this.objectMapper.registerModule(new OAuth2AuthorizationModule());
        /**
         * 这个问题可能是由于在没有 Spring 上下文的支持下实例化反序列化器所导致的。通过添加 SpringHandlerInstantiator，您可以确保 Jackson 在反序列化过程中能够正确地通过 Spring 的 beanFactory 实例化反序列化器。
         * 在您的代码中，通过设置 SpringHandlerInstantiator，Spring 的 beanFactory 能够帮助实例化这些没有默认构造函数的类，解决了实例化反序列化器失败的问题。下面详细解释一下为什么这样的设置会有效：
         * 1. 为什么缺少无参构造函数会导致反序列化失败
         * Jackson 默认在反序列化时会尝试使用无参构造函数创建对象。如果类没有无参构造函数，就会引发实例化异常。而使用 SpringHandlerInstantiator 可以告诉 Jackson 使用 Spring 的上下文创建这些对象，这样如果这些对象依赖于某些 Bean 或有参构造函数，Spring 的上下文就能够提供这些依赖。
         * 2. SpringHandlerInstantiator 的使用
         * SpringHandlerInstantiator 能够让 Jackson 与 Spring 的 BeanFactory 协同工作，确保需要特定构造函数或依赖注入的类能够被正确实例化。
         */
        this.objectMapper.setHandlerInstantiator(new SpringHandlerInstantiator(beanFactory));
    }

    @Override
    public void save(OAuth2AuthorizationConsent authorizationConsent) {
        Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
        int id = getId(authorizationConsent);
        set(RedisOAuth2Constants.getIdToConsent(id), write(authorizationConsent), 3600L);
    }

    @Override
    public void remove(OAuth2AuthorizationConsent authorizationConsent) {
        Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
        int id = getId(authorizationConsent);
        stringRedisTemplate.delete(RedisOAuth2Constants.getIdToConsent(id));
    }

    @Override
    @Nullable
    public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
        Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
        Assert.hasText(principalName, "principalName cannot be empty");
        int id = getId(registeredClientId, principalName);
        return Optional.ofNullable(get(RedisOAuth2Constants.getIdToConsent(id)))
                .map(this::parse)
                .orElse(null);
    }


    private static int getId(String registeredClientId, String principalName) {
        return Objects.hash(registeredClientId, principalName);
    }

    private static int getId(OAuth2AuthorizationConsent authorizationConsent) {
        return getId(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
    }

    private String write(Object data) {
        try {
            return this.objectMapper.writeValueAsString(data);
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex.getMessage(), ex);
        }
    }

    private OAuth2AuthorizationConsent parse(String data) {
        try {
            return this.objectMapper.readValue(data, new TypeReference<>() {
            });
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex.getMessage(), ex);
        }
    }

    private void set(String key, String value, long timeout) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    private String get(Object key) {
        return stringRedisTemplate.opsForValue().get(key);
    }
}
